# -*- coding: utf-8 -*-
"""
@File:    mail.py
@Author:  jk.dong
@Desc:    邮箱服务器接收，不同服务器之间的操作不同
"""
import traceback
import re
import email
from abc import abstractmethod
from email.utils import parseaddr, formataddr
from email.parser import Parser, BytesParser
import poplib
import imaplib
import smtplib
from docwork import logging, os, datetime, Message
from docwork.dao.receive_dao import RECV_CONF
from docwork.utils.crypto import decrypt
from docwork.utils.codecs import detect_charset, guess_charset, decode_str
from docwork.utils.file_process import save_body_source, save_file

# ========================邮件接收相关操作============================
BODY_SOURCE_REGION = RECV_CONF['receive-setting'].get('body-source')


class MailClient:
    def __init__(self, username, password, server, port, protocol) -> None:
        self.__username = username
        self.password = password
        self.__server = server
        self.__port = port
        self.protocol = protocol
        self.conn = None
        self.uid_list = []
        self.buffer_size = 20900

    # 设置为只读
    @property
    def username(self):
        return self.__username

    @property
    def server(self):
        return self.__server

    @property
    def port(self):
        return self.__port

    @abstractmethod
    def quit(self):
        raise NotImplementedError("Quit method must be implemented")

    def get_message_uid(self):
        return self.uid_list

    @staticmethod
    def get_content_type(msg_parse):
        content_type = msg_parse.get('content-type', '')
        if content_type != "":
            content_type = email.message._splitparam(content_type)[0].lower()
            if content_type.count('/') != 1:
                return ""
        return content_type

    @staticmethod
    def parse_body(msg, msg_parse, indent=0, text_plain=False):
        if msg_parse.is_multipart():
            parts = msg_parse.get_payload()
            rfc822 = False
            if indent == 1:
                content_type = MailClient.get_content_type(msg_parse)
                if content_type == "message/rfc822":
                    rfc822 = True
            if rfc822:
                for n, part in enumerate(parts):
                    MailClient.parse_body(msg, part, indent + 1, True)
            else:
                for n, part in enumerate(parts):
                    MailClient.parse_body(msg, part, indent + 1)
        else:
            """Return the message's content type.

            RFC 2045 defines a message's default type to be text/plain unless it
            appears inside a multipart/digest container, in which case it would be
            message/rfc822.
            """
            content_type = MailClient.get_content_type(msg_parse)
            if text_plain and content_type == 'text/plain':
                content_type = ''
            charset = detect_charset(msg_parse)
            email_content_type = ''
            if content_type == 'text/plain' or content_type == 'text/html':
                encode_name = msg_parse.get('Content-Transfer-Encoding', '').lower().strip()
                if encode_name and (encode_name == 'base64' or encode_name == 'quoted-printable'):
                    if charset:
                        if charset and isinstance(charset, str):
                            charset = charset.strip('"').strip("'")
                            if charset == "gb2312" or charset == 'gbk':
                                charset = "gb18030"
                        content = msg_parse.get_payload(decode=True).decode(charset, errors="ignore")
                    else:
                        content = msg_parse.get_payload(decode=True)
                        charset = guess_charset(content, '')
                        if charset:
                            content = content.decode(charset)
                        else:
                            content = content.decode()
                else:
                    '''
                        the payload will be decoded if this header's value is `quoted-printable' or `base64',
                        otherwise the payload is returned as-is.
                    '''
                    content = msg_parse.get_payload(decode=False)

                if content_type == 'text/plain':
                    email_content_type = 'text'
                    msg.body_text = content.strip()
                elif content_type == 'text/html':
                    email_content_type = 'html'
                    msg.body_html = content.strip()

                logging.info(f"{email_content_type}:\n{content[:100]}\n......")

    @staticmethod
    def parse_message(rawmaillog_id, msg_parse, parsehtml, attach_path=None):
        if not attach_path:
            attach_path = RECV_CONF['receive-setting'].get('rawattach_path')
        msg = Message(rawmaillog_id)
        try:
            for header in ['From', 'To', 'Cc', 'Subject', 'Received', 'Message-ID']:
                value = msg_parse.get(header, '')
                if header == 'Received':
                    date_str = ''
                    if msg_parse.get('Date', ''):
                        date_str = msg_parse.get('Date', '')
                    elif msg_parse.get('Received', ''):
                        date_str = msg_parse.get('Received', '').split(';')[-1].strip()
                    else:
                        msg.receive_time = datetime.datetime.now()
                    if date_str:
                        date_tuple = email.utils.parsedate_tz(date_str)
                        if date_tuple:
                            receive_time = datetime.datetime.fromtimestamp(email.utils.mktime_tz(date_tuple))
                            msg.receive_time = receive_time
                elif value:
                    if header == 'Subject':
                        value = decode_str(value)
                        msg.subject = value.strip()
                    elif header == 'Message-ID':
                        message_id = msg_parse.get('Message-ID', '')
                        if message_id:
                            msg.message_id = message_id.strip().strip('<>')
                    else:
                        hdr, addr = parseaddr(value)
                        name = decode_str(hdr)
                        if header == 'From':
                            msg.from_ = addr.strip()
                            msg.from_real_name = name.strip()
                        elif header == 'To':
                            msg.to_ = addr.strip()
                            msg.to_real_name = name.strip()
                        elif header == 'Cc':
                            msg.cc = addr.strip()
                            msg.cc_real_name = name.strip()
                        value = name + ' < ' + addr + ' > '
                logging.info(f"{header}:{value}")
            attach_index = 1

            # 嵌套解析邮件正文
            DirectConClient.parse_body(msg, msg_parse)  # 将Body分为bodyHtml和bodyText

            # 仅客户端执行，将msg.body_html内容作为附件发送
            if parsehtml and RECV_CONF['import-setting'].get('is_client'):
                if BODY_SOURCE_REGION.get('customer_list') and BODY_SOURCE_REGION.get('sender_list') \
                        and BODY_SOURCE_REGION.get('title_list'):
                    title_list = [i for i in BODY_SOURCE_REGION.get('title_list') if
                                  i in msg.subject]
                    sender_list = [i for i in BODY_SOURCE_REGION.get('sender_list') if
                                   i == msg.from_]
                    if title_list and sender_list:
                        save_body_source(msg, rawmaillog_id, save_path=attach_path)
                        attach_index += 1
                elif BODY_SOURCE_REGION.get('sender_list'):
                    sender_list = [i for i in BODY_SOURCE_REGION.get('sender_list') if
                                   i == msg.from_]
                    if BODY_SOURCE_REGION.get('title_list'):
                        title_list = [i for i in BODY_SOURCE_REGION.get('title_list') if
                                      i in msg.subject]
                        if title_list and sender_list:
                            save_body_source(msg, rawmaillog_id,
                                             save_path=attach_path)
                            attach_index += 1
                    else:
                        if sender_list:
                            save_body_source(msg, rawmaillog_id, save_path=attach_path)
                            attach_index += 1
                elif BODY_SOURCE_REGION.get('title_list'):
                    title_list = [i for i in BODY_SOURCE_REGION.get('title_list') if
                                  i in msg.subject]
                    if title_list:
                        save_body_source(msg, rawmaillog_id, save_path=attach_path)
                        attach_index += 1
                else:
                    save_body_source(msg, rawmaillog_id, save_path=attach_path)
                    attach_index += 1
            # 遍历解析附件
            for part in msg_parse.walk():
                filename = part.get_filename()
                content_type = part.get_content_type()
                if filename:
                    filename = decode_str(filename)
                    data = part.get_payload(decode=True)
                    content_id = ""
                    if content_type == 'image/jpeg':
                        content_id = part.get('Content-ID', "")
                    max_attach_title = RECV_CONF['receive-setting'].get('max_attach_title')
                    if max_attach_title and len(filename) >= int(max_attach_title):
                        r = filename.split('.')[-1]
                        length = int(max_attach_title) - len(r) - 1
                        filename = filename[:length] + '.' + r
                    if filename.rfind('.') >= 0:
                        # 处理邮件名
                        save_file(data,
                                  f'{rawmaillog_id}_{attach_index}{filename[filename.rfind('.'):]}',
                                  attach_path)
                        if content_type == 'image/jpeg':
                            msg.add_attachment(filename,
                                               attach_path / f'{rawmaillog_id}_{attach_index}{filename[
                                                                                              filename.rfind(
                                                                                                  "."):]}',
                                               content_id)
                        else:
                            msg.add_attachment(filename,
                                               attach_path / f'{rawmaillog_id}_{attach_index}{filename[
                                                                                              filename.rfind(
                                                                                                  '.'):]}')
                    else:
                        save_file(data, f'{rawmaillog_id}_{attach_index}',
                                  attach_path)
                        logging.info(f'Accessory: {rawmaillog_id}_{attach_index}')
                        if content_type == 'image/jpeg':
                            msg.add_attachment(filename,
                                               attach_path / f'{rawmaillog_id}_{attach_index}',
                                               content_id)
                        else:
                            msg.add_attachment(filename,
                                               attach_path / f'{rawmaillog_id}_{attach_index}')
                    attach_index += 1
        except Exception as err:
            logging.info(traceback.format_exc())
            logging.error(f'An error occurred while parsing message. Error:{err}')
        return msg

    @staticmethod
    def get_message_from_eml(filename, rawmaillog_id, eml_dir):
        # 列出eml_dir下的所有文件
        filenames = [item.name for item in eml_dir.iterdir() if item.name.split('_')[1] == str(filename)]
        if len(filenames) != 1:
            logging.error(f"无法准确找到eml文件，模糊匹配到的文件数为：{len(filenames)}")
            return None
        target_eml = eml_dir.joinpath(f"{filenames[0]}.eml")
        logging.info(f"=========>本地eml文件：{target_eml}")
        if target_eml.exists():
            # 读取eml文件并解析
            with open(target_eml, 'r') as fp:
                content = email.message_from_string(fp.read())
                # logging.info(f"eml file content: {content}")
            msg_structure = content
            return DirectConClient.parse_message(rawmaillog_id, msg_structure, parsehtml=BODY_SOURCE_REGION.get('send'))
        else:
            logging.error("无本地邮件")
            return None


class DirectConClient(MailClient):
    def __init__(self, username, password, server, port, protocol):
        super().__init__(username, password, server, port, protocol)
        if protocol == 'POP3':
            poplib._MAXLINE = 20480
            if RECV_CONF['receive-setting'].get('protocol_ssl'):
                self.conn = poplib.POP3_SSL(server, port)
            else:
                self.conn = poplib.POP3(server, port)
            self.conn.user(username)
            self.conn.pass_(password)
        elif protocol == 'IMAP':
            if RECV_CONF['receive-setting'].get('protocol_ssl'):
                self.conn = imaplib.IMAP4_SSL(port=self.port, host=self.server)
            else:
                self.conn = imaplib.IMAP4(port=self.port, host=self.server)
            self.conn.login(self.username, self.password)
            self.conn.select()
            typ, mail_data = self.conn.search(None, 'ALL')
            self.mail_data = mail_data
        
    def _delete(self, index):
        try:
            if self.protocol == 'POP3':
                self.conn.dele(str(index))
            elif self.protocol == 'IMAP':
                self.conn.store(str(index), '+FLAGS', '\\Deleted')
                self.conn.expunge()
            logging.info("邮件删除成功！")
        except Exception as err:
            logging.info(traceback.format_exc())
            logging.error(f"邮件删除失败, Error: {err}.")

    def quit(self):
        if self.protocol == 'POP3':
            self.conn.quit()
        elif self.protocol == 'IMAP':
            self.conn.close()
            self.conn.logout()

    def generate_eml_name(self, msg_obj, rawmaillog_id):
        suffix = ''
        if msg_obj.message_id:
            suffix = msg_obj.message_id.split('@')[0]
        if msg_obj.receive_time:
            suffix = msg_obj.receive_time.strftime("%Y%m%d") + f'_{rawmaillog_id}_{suffix}'
        return suffix + f'@{self.server}.eml'

    def get_message_uid(self):
        if self.protocol == "POP3":
            self.uid_list = [uid.decode().split(' ')[1] for uid in self.conn.uidl()[1]]
            return self.uid_list
        elif self.protocol == 'IMAP':
            # 以下代码等价于self.conn.search(None,"ALL")：None表示默认utf8字符集，ALL表示获取全部的邮件
            result, data = self.conn.uid('search', None, "ALL")
            self.uid_list = [self.username + '@' + self.server + '_' + uid.decode() for uid in data[0].split()]
            return self.uid_list

    def get_message(self, index, rawmaillog_id):
        msg_structure = {}
        logging.info(f'获取{self.protocol}协议的邮件信息')
        if self.protocol == 'POP3':
            resp, lines, octets = self.conn.retr(index)
            # 邮件取出的信息是bytes，转换成Parser支持的str
            list_raw_msg = []
            index = 0
            for line in lines:
                index += 1
                try:
                    charset = guess_charset(line, 'gb18030')
                    if charset:
                        list_raw_msg.append(line.decode(charset))
                    else:
                        list_raw_msg.append(line.decode())
                except Exception as err:
                    logging.error(f'raw mail line:{index} decode ERROR:{err}')
            str_raw_msg = os.linesep.join(list_raw_msg)
            msg_structure = Parser().parsestr(str_raw_msg)
            msg_object = self.parse_message(rawmaillog_id, msg_structure, parsehtml=BODY_SOURCE_REGION.get('send'))
            save_filename = self.generate_eml_name(msg_object, rawmaillog_id)
            save_file(os.linesep.encode().join(lines), save_filename,
                      RECV_CONF['receive-setting'].get('rawmail_path'))
            return msg_object
        elif self.protocol == 'IMAP':
            if not self.uid_list:
                self.uid_list = self.get_message_uid()
            index = self.uid_list[int(index) - 1].split('_')[-1]
            resp, lines = self.conn.uid('FETCH', str(index), '(RFC822)')
            msg_structure = email.message_from_string(lines[0][1].decode('utf-8'))
            msg_object = self.parse_message(rawmaillog_id, msg_structure, parsehtml=BODY_SOURCE_REGION.get('send'))
            save_filename = self.generate_eml_name(msg_object, rawmaillog_id)
            save_file(str(msg_structure), save_filename,
                      RECV_CONF['receive-setting'].get('rawmail_path'))
            return msg_object


class HttpClient(MailClient):
    """
    以http接口方式获取邮件信息（包括EWS、CoreMail协议）
    """
    def __init__(self, username, password, server, port, protocol) -> None:
        super().__init__(username, password, server, port, protocol)
        if protocol == 'EWS':
            from exchangelib import DELEGATE, Credentials, Account, Configuration
            from exchangelib.protocol import BaseProtocol, NoVerifyHTTPAdapter
            BaseProtocol.HTTP_ADAPTER_CLS = NoVerifyHTTPAdapter
            creds = Credentials(username=username, password=password)
            ews_config = Configuration(service_endpoint=RUN_CONF.receiver_config.ews_url, credentials=creds)
            self.account = Account(username, RUN_CONF=ews_config, autodiscover=False, access_type=DELEGATE)
        elif protocol == 'COREMAIL':
            pass
            # 验证，登录
            # verify(domain=server)
            # login(server, username, password)


# ========================邮件发送相关操作============================
def sendmail(msg, config):
    """
    以smtp协议发送邮件
    """
    from email import encoders
    from email.header import Header
    from email.mime.multipart import MIMEMultipart
    from email.mime.base import MIMEBase
    from email.mime.text import MIMEText
    from email.mime.application import MIMEApplication

    send_ssl = config['send-setting'].get('smtp_ssl')
    smtp_server = config['send-setting'].get('smtp_server')
    port = config['send-setting'].get('port')
    sendmail_login_name = config['send-setting'].get('sendmail_login_name')
    sendmail_password = config['send-setting'].get('sendmail_password')
    if config['send-setting'].get('sendmail_password_encrypted'):
        sendmail_password = decrypt(sendmail_password, config_=config['run-setting'])
    sendmail_from_address = config['send-setting'].get('sendmail_from_address')
    mail_attach_pact = config['send-setting'].get('mail_attach_pact')

    def format_addr_(str_addr):
        name, addr = parseaddr(str_addr)
        return formataddr((Header(name, 'utf-8').encode(), addr))

    try:
        if config['send-setting'].get('send_protocol') == 'SMTP':
            if send_ssl:
                smtp_obj = smtplib.SMTP_SSL(smtp_server, port)
            else:
                smtp_obj = smtplib.SMTP(smtp_server, port)
            # 获取本机的FQDN（完全限定的域名）
            smtp_obj.ehlo()
            # STARTTLS是一种把已经存在的一条不安全的链接,用SSL/TLS的加密方法,把这条不安全的连接升级成安全的连接
            if smtp_obj.has_extn('STARTTLS'):
                smtp_obj.starttls()
                smtp_obj.ehlo()
            if config['send-setting'].get('smtp_login'):
                smtp_obj.login(sendmail_login_name, sendmail_password)
            message = MIMEMultipart()
            message["Accept-Language"] = "zh-CN"
            message["Accept-Charset"] = "ISO-8859-1,utf-8"
            message['From'] = format_addr_('%s<%s>' % (msg.get('sender'), sendmail_from_address))
            message['To'] = format_addr_(msg.get('receiver'))
            message['Subject'] = Header(msg.get('title'), 'utf-8')
            date = msg.get('date')
            if date:
                if isinstance(date, str):
                    date = datetime.datetime.strptime(date, '%Y-%m-%d %H:%M:%S').strftime("%a, %d %b %Y %H:%M:%S %z+0800")
                else:
                    date = date.strftime("%a, %d %b %Y %H:%M:%S %z+0800")
            else:
                date = datetime.datetime.now().strftime("%a, %d %b %Y %H:%M:%S %z+0800")
            message['Date'] = date
            if msg.get('attach'):
                for attach in msg.get('attach'):
                    if mail_attach_pact and mail_attach_pact == 'base':
                        att = MIMEBase('application', 'octet-steam')
                        att.set_payload(open(attach[1], 'rb').read())
                        att.set_param('name', Header(attach[0], 'utf-8').encode())
                        encoders.encode_base64(att)
                    else:
                        att = MIMEApplication(open(attach[1], 'rb').read())
                        att["Content-Type"] = attach[2]
                        att.set_param('name', Header(attach[0], 'utf-8').encode())
                    att.add_header("Content-Disposition", "attachment", filename=Header(attach[0], 'utf-8').encode())
                    message.attach(att)
            message.attach(MIMEText(msg.get('body'), 'plain', 'utf-8'))
            smtp_obj.sendmail(sendmail_from_address, msg.get('receiver'), message.as_string())
            smtp_obj.quit()
            return True
        elif config['send-setting'].get('send_protocol') == 'EWS':
            from exchangelib import Mailbox, FileAttachment
            from exchangelib import Message as Message_
            to_recipients = []
            for recipient in re.split(r',', msg.get('receiver')):
                to_recipients.append(Mailbox(email_address=recipient))
            client = MailClient(sendmail_login_name, sendmail_password, config['import-setting'].get('ews_url'),
                                int(port), config['import-setting'].get('send_protocol'))
            # Create message
            m = Message_(account=client.account,
                        subject=msg.get('title'),
                        body=msg.get('body'),
                        to_recipients=to_recipients)

            if msg.get('attach'):
                for attach in msg.get('attach'):
                    with open(attach[1], 'rb') as f:
                        file = FileAttachment(name=attach[0], content=f.read())
                        m.attach(file)
            m.send()
            return True
        else:
            logging.error(f"发送协议为：{config['send-setting'].get('send_protocol')}，不符合要求，请检查配置文件send_protocol字段。")
    except Exception as err:
        logging.info(traceback.format_exc())
        logging.error(f"邮件发送出现异常(func:sendmail()). Error:{err}")
        return False


def send_email_from_eml(eml_file_path, smtp_server, smtp_port, login, password):
    from email import policy
    # 读取 .eml 文件内容
    with open(eml_file_path, 'rb') as f:
        eml_data = f.read()

    # 解析 .eml 文件为 Message 对象
    msg = BytesParser(policy=policy.default).parsebytes(eml_data)

    # 提取发件人和收件人（假设只有一个收件人）
    from_addr = '178276549@qq.com'
    # to_addrs = msg['To'].split(',')  # 处理多个收件人的情况
    to_addr = '19120552136@163.com'  # 这里只取第一个收件人作为示例

    # 连接到 SMTP 服务器
    server = smtplib.SMTP(smtp_server, smtp_port)
    server.starttls()  # 启用 TLS 加密
    server.login(login, password)  # 登录 SMTP 服务器

    # 发送邮件
    server.sendmail(from_addr, to_addr, msg.as_string())

    # 关闭连接
    server.quit()

    return True

